iT邦幫忙

2025 iThome 鐵人賽

DAY 23
0
Modern Web

Angular、React、Vue 三框架實戰養成:從零打造高品質前端履歷網站系列 第 23

Day 23 Vue Router – 作品詳情頁與路由導覽

  • 分享至 

  • xImage
  •  

今日目標

  • 安裝並設定 vue-router
  • 在卡片使用 <RouterLink> 導到 /projects/:slug
  • 在詳情頁用 useRoute() 讀取 :slug,從本地資料查單筆內容
  • 補上 404 頁(找不到 slug)
  • (可選)子路由:/projects/:slug/info/projects/:slug/gallery

1) 安裝與初始化 Router

npm i vue-router@4

建立 src/router/index.ts

// src/router/index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import Home from '@/views/Home.vue'
import ProjectDetail from '@/views/ProjectDetail.vue'
import NotFound from '@/views/NotFound.vue'

const routes: RouteRecordRaw[] = [
  { path: '/', name: 'home', component: Home },
  { path: '/projects/:slug', name: 'project-detail', component: ProjectDetail, props: true },
  { path: '/:pathMatch(.*)*', name: 'not-found', component: NotFound }
]

export const router = createRouter({
  history: createWebHistory(), // 若部署在子目錄,改 createWebHistory('/子路徑/')
  routes,
  scrollBehavior() {
    return { top: 0 }
  }
})

把 router 掛到 app(修改 src/main.ts):

import { createApp } from 'vue'
import App from './App.vue'
import { router } from './router'
import './styles/base.css'

createApp(App).use(router).mount('#app')


2) 視圖與組件拆分

建立 src/views/Home.vue,把原本 App.vue 內主要內容移到這裡(Header/Footer 繼續在 App.vue):

<!-- src/views/Home.vue -->
<template>
  <main id="home">
    <Hero />
    <About />
    <Skills />
    <Projects />
    <Contact />
  </main>
</template>

<script setup lang="ts">
import Hero from '@/components/Hero.vue'
import About from '@/components/About.vue'
import Skills from '@/components/Skills.vue'
import Projects from '@/components/Projects.vue'
import Contact from '@/components/Contact.vue'
</script>

App.vue 改為只負責框架與 router 入口:

<!-- src/App.vue -->
<template>
  <SiteHeader />
  <RouterView />
  <SiteFooter />
</template>

<script setup lang="ts">
import { RouterView } from 'vue-router'
import SiteHeader from '@/components/SiteHeader.vue'
import SiteFooter from '@/components/SiteFooter.vue'
</script>


3) 列表卡片改用 <RouterLink> 導向詳情頁

修改 src/components/Projects.vue:把 demo / repo 留著,同時加一個「查看詳情」連到 /projects/:slug

<!-- 片段:src/components/Projects.vue -->
<article class="card" v-for="p in view" :key="p.id">
  <h3>{{ p.title }}</h3>
  <p class="muted">{{ p.tech }}</p>
  <p>{{ p.desc }}</p>
  <div class="actions" style="display:flex; gap:8px; margin-top:8px;">
    <RouterLink class="btn small" :to="{ name:'project-detail', params:{ slug: p.slug } }">
      查看詳情
    </RouterLink>
    <a class="btn small btn-outline" :href="p.repo" target="_blank" rel="noopener">GitHub</a>
  </div>
</article>

為了 SSR / SEO 的一致性,建議只把「站內導覽」用 ,外部連結維持 。


4) 詳情頁:讀 slug 並顯示單筆專案

建立 src/views/ProjectDetail.vue

<template>
  <section class="container section" v-if="project">
    <nav style="margin-bottom:12px;">
      <RouterLink to="/" class="btn btn-outline">← 返回列表</RouterLink>
    </nav>

    <h2>{{ project.title }}</h2>
    <p class="muted">{{ project.tech }}</p>

    <div class="gallery" v-if="project.images?.length" style="display:flex; gap:12px; flex-wrap:wrap; margin:12px 0;">
      <img v-for="src in project.images" :key="src" :src="src" alt="專案截圖" width="360" />
    </div>

    <p>{{ project.desc }}</p>

    <div class="actions" style="display:flex; gap:8px; margin-top:8px;">
      <a class="btn" :href="project.demo" target="_blank" rel="noopener">Live Demo</a>
      <a class="btn btn-outline" :href="project.repo" target="_blank" rel="noopener">GitHub</a>
    </div>
  </section>

  <section class="container section" v-else>
    <h2>找不到這個專案</h2>
    <p class="muted">請回到列表,或確認網址是否正確。</p>
    <RouterLink to="/" class="btn">返回列表</RouterLink>
  </section>
</template>

<script setup lang="ts">
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import { projects } from '@/data/projects'

const route = useRoute()
const slug = computed(() => String(route.params.slug || ''))
const project = computed(() => projects.find(p => p.slug === slug.value))
</script>


5) 404 頁(NotFound)

新增 src/views/NotFound.vue

<template>
  <section class="container section">
    <h2>404 找不到頁面</h2>
    <p class="muted">你要找的頁面不存在,或已被移動。</p>
    <RouterLink to="/" class="btn">回首頁</RouterLink>
  </section>
</template>

路由中已加入 { path: '/:pathMatch(.)', component: NotFound }。


(可選)6) 子路由:/projects/:slug/info/projects/:slug/gallery

如果想把詳情切成分頁(資訊/圖片):

  • 新增 src/views/ProjectInfo.vuesrc/views/ProjectGallery.vue(可重用上面 ProjectDetail 裡的片段)
  • 調整路由(src/router/index.ts):
import ProjectInfo from '@/views/ProjectInfo.vue'
import ProjectGallery from '@/views/ProjectGallery.vue'

{
  path: '/projects/:slug',
  component: ProjectDetail, // 當父頁,內含次級 <RouterView />
  props: true,
  children: [
    { path: '', redirect: { name: 'project-info' } },
    { path: 'info', name: 'project-info', component: ProjectInfo, props: true },
    { path: 'gallery', name: 'project-gallery', component: ProjectGallery, props: true }
  ]
}

ProjectDetail.vue 中加入子導航與 <RouterView />

<nav class="sub-nav" style="display:flex; gap:12px; margin:12px 0;">
  <RouterLink :to="{ name:'project-info', params:{ slug } }" active-class="active">資訊</RouterLink>
  <RouterLink :to="{ name:'project-gallery', params:{ slug } }" active-class="active">圖片</RouterLink>
</nav>
<RouterView />


成果檢查清單

  • / 顯示 Home(含 Projects 列表)
  • 點某張卡片 → /projects/:slug 看到該專案詳細資料
  • 輸入不存在的 slug → 顯示「找不到專案」或 404 頁
  • (可選)子路由可於詳情頁切換 info / gallery

小心踩雷(常見誤用 → 正確作法)

  1. 把內容寫回 index.html
    • ❌ 動手改 index.html 放頁面
    • ✅ 所有頁面放在 views/,由 Router 管
  2. 直接用 <a href="/xxx"> 做站內導覽
    • ❌ 會整頁重載
    • ✅ 用 <RouterLink :to="...">(SPA 體驗、保留狀態)
  3. 忘了把 router 掛到 app
    • createApp(App).mount('#app')
    • createApp(App).use(router).mount('#app')
  4. 找不到圖片
    • 確認 assets 路徑;Vite 會把 src/assets 內的資源處理成相對路徑。

下一步(Day 24 預告)

表單與驗證(Vue 版)

  • v-model + 自訂驗證 改良 Contact 表單(即時錯誤、送出前檢查)
  • (可選)導入 Vuelidate / vee-validate 做更完整的規則
  • 把「送出成功」做成小彈窗或 toast,體驗更完整 ✨

上一篇
Day 22 Vue 資料綁定 – 用 v-for 渲染清單 + 分類與搜尋
下一篇
Day 24 表單與驗證(Vue 版)— v-model + 自訂驗證 + Toast 成功提示
系列文
Angular、React、Vue 三框架實戰養成:從零打造高品質前端履歷網站24
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言